https://www.d3indepth.com/introduction/Introduction to D3
D3 is a JavaScript library used to create bespoke, interactive charts and maps on the web. While most charting libraries (such as Chart.js and Highcharts) provide ready made charts D3 consists of a large set of building blocks from which custom charts or maps can be constructed. D3’s approach is much lower level than other charting libraries. Creating a bar chart with Chart.js is just a few lines of code. Creating the same chart with D3 you need to: . create SVGrect
elements and join them to the data . position therect
elements . size therect
elements according to the data . add axes You might also want to: . animate the bars when the chart first loads . adapt the chart to the container size . add a tooltip This is all additional effort but it gives you complete control over the chart’s appearance and behaviour. If a standard bar, line or pie chart is sufficient you should consider a library such as Chart.js. However if you wish to create a bespoke chart to an exact specification then D3 is worth considering. D3’s features include: . data-driven modification of HTML/SVG elements . loading and transforming data (e.g. CSV data) . generation of complex charts such as treemaps, packed circles and networks . a powerful transition system for animating between views . powerful user interaction support, including panning, zooming and draggingData-driven modification of HTML/SVG elements
Given an array of objects such as:With D3 we can: . add/remove[ { "name": "Andy", "score": 37 }, { "name": "Beth", "score": 39 }, { "name": "Craig", "score": 31 }, { "name": "Diane", "score": 35 }, { "name": "Evelyn", "score": 38 } ]
div
elements based on the array length . add a label and bar to eachdiv
element . update the width of the bar based on the person’s score View source | Edit in GistRunData transformation
D3 provides many functions for manipulating data. For example it has functions to request CSV (comma separated value) data and transform it into an array of objects. Suppose you have a CSV file named films.csv on your server:you can request it using:Film,Genre,Lead Studio,Audience score %,Worldwide Gross,Year 27 Dresses,Comedy,Fox,71,160.308654,2008 (500) Days of Summer,Comedy,Fox,81,60.72,2009 A Dangerous Method,Drama,Independent,89,8.972895,2011 A Serious Man,Drama,Universal,64,30.68,2009 Across the Universe,Romance,Independent,84,29.367143,2007 Beginners,Comedy,Independent,80,14.31,2011
D3 transforms the CSV data into an array of objects:d3.csv('films.csv',function(err,data){ // Do something with the data })
Notice that D3 has used the CSV column names ([ { "Film": "27 Dresses", "Genre": "Comedy", "Lead Studio": "Fox", "Audience score %": "71", "Worldwide Gross": "160.308654", "Year": "2008" }, { "Film": "(500) Days of Summer", "Genre": "Comedy", "Lead Studio": "Fox", "Audience score %": "81", "Worldwide Gross": "60.72", "Year": "2009" }, { "Film": "A Dangerous Method", "Genre": "Drama", "Lead Studio": "Independent", "Audience score %": "89", "Worldwide Gross": "8.972895", "Year": "2011" }, { "Film": "A Serious Man", "Genre": "Drama", "Lead Studio": "Universal", "Audience score %": "64", "Worldwide Gross": "30.68", "Year": "2009" }, { "Film": "Across the Universe", "Genre": "Romance", "Lead Studio": "Independent", "Audience score %": "84", "Worldwide Gross": "29.367143", "Year": "2007" }, { "Film": "Beginners", "Genre": "Comedy", "Lead Studio": "Independent", "Audience score %": "80", "Worldwide Gross": "14.31", "Year": "2011" } ]
Film
,Genre
,Lead Studio
etc.) as property names for each object. (The CSV file is from Information is Beautiful.)Shape generation
D3 is probably best known for its role in producing interactive data visualisations. These are typically made of up SVG (Scalable Vector Graphic) elements such asline
,circle
,path
andtext
. Suppose you have co-ordinateswhich you’d like to connect with lines. D3 can generate the SVG: View source You can choose to interpolate the points with a curve: View source D3 can also create axes: View source As with most D3 elements, you have a lot of configuration available. For example you can change the axis orientation as well as the number and format of the tick marks: View sourcevar data=[[0,50],[100,80],[200,40],[300,60],[400,30]];
Layouts
D3 provides a number of layouts which are functions that help transform your data into a visual layout. For example, if we have hierarchical (or tree shaped) data, we can use layouts to create a tree view: View source a packed circle view (with leaf nodes sized by revenue): View source and a treemap: View source Under the hood, a layout adds properties (such as position, radius, width and height) to each data element. These properties can then be used when updating the DOM elements.Transitions
D3 makes it easy to introduce a transition effect between DOM states. Not only can position and size (e.g. width, height, radius) be smoothly transitioned, but also colours: View source As well as producing pleasing visual effects, transitions help users keep track of elements between different states.User interaction
D3 has some useful tools to enable effect user interaction such as voronoi grids (to optimise hover/click/touch areas), brushing, zooming and dragging. For example, suppose we have a number of small points with a hover-over effect, it’s quite hard to position the mouse pointer exactly over a circle: View source However if the voronoi grid is enabled (click ‘Enable Voronoi’ above) polygons are enabled which help determine the closest point to the user’s hover/click/touch. It’s now much easier to locate a point. (Click ‘View Voronoi’ to see the underlying polygons.)Selections
D3 selections allow DOM elements to be selected in order to do something with them, be it changing style, modifying their attributes, performing data-joins or inserting/removing elements. For example, given 5 circles: we can used3.selectAll
to select the circles and.style
and.attr
to modify them:d3.selectAll('circle') .style('fill','orange') .attr('r',function(){ return10+Math.random()*40; });
Making selections
D3 has two functions to make selectionsd3.select
andd3.selectAll
.d3.select
selects the first matching element whilstd3.selectAll
selects all matching elements. Each function takes a single argument which specifies the selector string. For example to select all elements with classitem
used3.selectAll('.item')
.Modifying elements
Once we’ve made a selection we can modify the elements in it using the following functions:
Name | Behaviour | Example |
---|---|---|
.style | Update the style | d3.selectAll('circle').style('fill', 'red') |
.attr | Update an attribute | d3.selectAll('rect').attr('width', 10) |
.classed | Add/remove a class attribute | d3.select('.item').classed('selected', true) |
.property | Update an element's property | d3.selectAll('.checkbox').property('checked', false) |
.text | Update the text content | d3.select('div.title').text('My new book') |
.html | Change the html content | d3.select('.legend').html('<div class="block"></div><div>0 - 10</div>') |
.select
or .selectAll
is used, all elements in the selection will be modified.
Here’s an example of all of these functions in use:
View source
red
, 10
and true
to .style
, .attr
, .classed
, .property
, .text
and .html
we can pass in a function:
d3.selectAll('circle')
.attr('cx',function(d,i){
returni*100;
});
The function typically accepts two arguments d
and i
.
The first argument d
is the joined data (see the data joins section) and i
is the index of the element within the selection.
If we want to update elements in a selection according to their position within the selection, we can use the i
argument.
For example to position some rect
elements horizontally we can use:
d3.selectAll('rect')
.attr('x',function(d,i){
returni*40;
});
View source
In the majority of cases when functions are passed in, anonymous functions are used.
However we can also use named functions e.g.
functionpositionRects(d,i){
returni*40;
}
d3.selectAll('rect')
.attr('x',positionRects);
.on
which expects a callback function into which is passed two arguments d
and i
.
As before, d
is the joined data (see the data joins section) and i
is the index of the element within the selection.)
The most common events include (see MDN event reference for more details):
Event name | Description |
---|---|
click | Element has been clicked |
mouseenter | Mouse pointer has moved onto the element |
mouseover | Mouse pointer has moved onto the element or its children |
mouseleave | Mouse pointer has moved off the element |
mouseout | Mouse pointer has moved off the element or its children |
mousemove | Mouse pointer has moved over the element |
d3.selectAll('circle')
.on('click',function(d,i){
d3.select('.status')
.text('You clicked on circle '+i);
});
View source
In the event callback function the this
variable is bound to the DOM element.
This allows us to do things such as:
d3.selectAll('circle')
.on('click',function(d,i){
d3.select(this)
.style('fill','orange');
});
View source
Note that this
is a DOM element and not a D3 selection so if we wish to modify it using D3 we must first select it using d3.select(this)
.
.append
and .insert
whilst elements can be removed using .remove
.
.append
appends an element to the children of each element in the selection.
The first argument specifies the type of element.
As an example let’s start with 3 g
elements, each containing a circle
:
<gclass="item"transform="translate(0, 0)">
<circler="40"/>
</g>
<gclass="item"transform="translate(120, 0)">
<circler="40"/>
</g>
<gclass="item"transform="translate(240, 0)">
<circler="40"/>
</g>
We can append a text
element to each using:
d3.selectAll('g.item')
.append('text')
.text(function(d,i){
returni+1;
});
resulting in a text
being added to each g.item
:
<gclass="item"transform="translate(0, 0)">
<circler="40"/>
<text>1</text>
</g>
<gclass="item"transform="translate(120, 0)">
<circler="40"/>
<text>2</text>
</g>
<gclass="item"transform="translate(240, 0)">
<circler="40"/>
<text>3</text>
</g>
View source
(.append
is commonly used in the context of enter/exit where it has different behaviour.)
.insert
is similar to .append
but it allows us to specify a before
element to which, you guessed it, the new element is attached.
Therefore if we run the same example again, but choosing to insert the text element before the circle element we get:
d3.selectAll('g.item')
.insert('text','circle')
.text(function(d,i){
returni+1;
});
and the DOM will look like:
<gclass="item"transform="translate(0, 0)">
<text>1</text>
<circler="40"/>
</g>
<gclass="item"transform="translate(120, 0)">
<text>2</text>
<circler="40"/>
</g>
<gclass="item"transform="translate(240, 0)">
<text>3</text>
<circler="40"/>
</g>
View source
.remove
removes all the elements in a selection.
For example, given some circles, we can remove them using:
d3.selectAll('circle')
.remove();
View source
.style
, .attr
and .on
can be chained:
d3.selectAll('circle')
.style('fill','orange')
.attr('r',20)
.on('click',function(d,i){
d3.select('.status')
.text('You clicked on circle '+i);
});
View source
.each
allows a function to be called on each element of a selection and .call
allows a function to be called on the selection itself.
In the case of .each
D3 passes in the joined datum (usually represented by d
) and the index (usually represented by i
).
Not only can .each
enable reusable components but it also allows computations to be shared across calls to .style
, .attr
etc.
Here’s an example of using .each
to call a reusable component:
functionaddNumberedCircle(d,i){
d3.select(this)
.append('circle')
.attr('r',40);
d3.select(this)
.append('text')
.text(i+1)
.attr('y',50)
.attr('x',30);
}
d3.selectAll('g.item')
.each(addNumberedCircle);
View source
Here’s an example of .each
used for the latter:
d3.selectAll('circle')
.each(function(d,i){
var odd=i%2===1;
d3.select(this)
.style('fill',odd?'orange':'#ddd')
.attr('r',odd?40:20);
});
View source
In the case of .call
D3 passes in the selection itself.
This is a common pattern for reusable components.
In the following example we create a similar component to before using .call
.
This time the selection gets passed into the component (rather than d
and i
):
functionaddNumberedCircle(selection){
selection
.append('circle')
.attr('r',40);
selection
.append('text')
.text(function(d,i){
returni+1;
})
.attr('y',50)
.attr('x',30);
}
d3.selectAll('g.item')
.call(addNumberedCircle);
.filter
.
A function is usually passed into .filter
which returns true
if the element should be included.
.filter
returns the filtered selection.
In this example we filter through even-numbered elements and colour them orange:
d3.selectAll('circle')
.filter(function(d,i){
returni%2===0;
})
.style('fill','orange');
View source
Sorting only really makes sense if data has been joined to the selection, so please read up on data joins first.
We can sort elements in a selection by calling .sort
and passing in a comparator function.
The comparator function has two arguments, usually a
and b
, which represent the datums on the two elements being compared.
If the comparator function returns a negative number, a
will be placed before b
and if positive, a
will be placed after b
.
Thus if we have the following data joined to a selection:
[
{
"name": "Andy",
"score": 37
},
{
"name": "Beth",
"score": 39
},
{
"name": "Craig",
"score": 31
},
{
"name": "Diane",
"score": 35
},
{
"name": "Evelyn",
"score": 38
}
]
we can sort by score
using:
d3.selectAll('.person')
.sort(function(a,b){
returnb.score-a.score;
});
View source
<circler="40"/>
<circler="40"cx="120"/>
<circler="40"cx="240"/>
<circler="40"cx="360"/>
<circler="40"cx="480"/>
and some data:
var scores=[
{
"name":"Andy",
"score":25
},
{
"name":"Beth",
"score":39
},
{
"name":"Craig",
"score":42
},
{
"name":"Diane",
"score":35
},
{
"name":"Evelyn",
"score":48
}
]
we can select the circles and then join the array to it:
d3.selectAll('circle')
.data(scores);
We can now manipulate the circles according to the joined data:
d3.selectAll('circle')
.attr('r',function(d){
returnd.score;
});
The above code sets the radius of each circle to each person’s score.
View source
myData
and a selection s
a data join is created using the function .data
:
var myData=[10,40,20,30];
var s=d3.selectAll('circle');
s.data(myData);
The array can contain any type e.g.
objects:
var cities=[
{name:'London',population:8674000},
{name:'New York',population:8406000},
{name:'Sydney',population:4293000}
];
var s=d3.selectAll('circle');
s.data(cities);
Although a couple of things occur when .data
is called (see Under the Hood and Enter/Exit) you probably won’t notice much change after joining your data.
The real magic happens when you want to modify the elements in your selection according to your data.
.style
and .attr
(which we covered in Selections):
d3.selectAll('circle')
.attr('r',function(d){
returnd;
});
For each element in the selection D3 will call this function, passing in the element’s joined data as the first argument d
.
The function’s return value is used to set the style or attribute value.
For example, given some circles:
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
and some data:
var myData=[10,40,20,30,50];
let’s perform the data join:
var s=d3.selectAll('circle');
// Do the join
s.data(myData);
Now let’s update the radius of each circle in the selection to be equal to the corresponding data values:
s.attr('r',function(d){
returnd;
});
The function that’s passed into .attr
is called 5 times (once for each element in the selection).
The first time round d
will be 10 and so the circle’s radius will be set to 10.
The second time round it’ll be 40 and so on.
In the above example the function simply returns d
meaning that the first circle’s radius will be set to 10, the second’s radius to 40 and so.
We can return anything we like from the function, so long as it’s a valid value for the style, attribute etc.
that we’re modifying.
(It’s likely that some expression involving d
will be returned.)
For example we can set the radius to twice d
using:
s.attr('r',function(d){
return2*d;
});
Now let’s set a class on each element if the value is greater or equal to 40:
s.classed('high',function(d){
returnd>=40;// returns true or false
});
and finally we’ll position the circles horizontally using the i
argument (see Selections):
s.attr('cx',function(d,i){
returni*120;
});
Putting this all together we get:
var myData=[10,40,20,30,50];
var s=d3.selectAll('circle');
// Do the data join
s.data(myData);
// Modify the selected elements
s.attr('r',function(d){
returnd;
})
.classed('high',function(d){
returnd>=40;
})
.attr('cx',function(d,i){
returni*120;
});
View source
var cities=[
{name:'London',population:8674000},
{name:'New York',population:8406000},
{name:'Sydney',population:4293000},
{name:'Paris',population:2244000},
{name:'Beijing',population:11510000}
];
var s=d3.selectAll('circle');
s.data(cities);
Now when we modify elements based on the joined data, d
will represent the joined object.
Thus for the first element in the selection, d
will be { name: 'London', population: 8674000}
.
Let’s set the circle radii proportionally to each city’s population:
s.attr('r',function(d){
var scaleFactor=0.000005;
returnd.population*scaleFactor;
})
.attr('cx',function(d,i){
returni*120;
});
View source
Of course, we not restricted to modifying circle
elements.
Supposing we had some rect
and text
elements, we can build a simple bar chart using what we’ve learnt:
var cities=[
{name:'London',population:8674000},
{name:'New York',population:8406000},
{name:'Sydney',population:4293000},
{name:'Paris',population:2244000},
{name:'Beijing',population:11510000}
];
// Join cities to rect elements and modify height, width and position
d3.selectAll('rect')
.data(cities)
.attr('height',19)
.attr('width',function(d){
var scaleFactor=0.00004;
returnd.population*scaleFactor;
})
.attr('y',function(d,i){
returni*20;
})
// Join cities to text elements and modify content and position
d3.selectAll('text')
.data(cities)
.attr('y',function(d,i){
returni*20+13;
})
.attr('x',-4)
.text(function(d){
returnd.name;
});
View source
__data__
to each DOM element in the selection and assigns the joined data to it.
We can inspect this in Google Chrome by right clicking on an element and choosing Inspect.
This’ll reveal Chrome’s debug window.
Look for a tab named ‘Properties’ and open it.
Expand the element then expand the __data__
attribute.
This is the data that D3 has joined to the element.
(See screencast.)
var featureCollection={type:'FeatureCollection',features:features};
we can join it to a single element using .datum
:
d3.select('path#my-map')
.datum(featureCollection);
This just adds a __data__
attribute to the element and assigns the joined data (featureCollection
in this case) to it.
See the geographic visualisations section for a deeper look at this.
Most of the time .data
will be used for data joins.
.datum
is reserved for special cases such as the above.
<divid="content">
<div></div>
<div></div>
<div></div>
</div>
and some data:
var myData=[10,40,20];
we join the array to the div
elements using:
d3.select('#content')
.selectAll('div')
.data(myData);
In this example myData
is the same length as the selection.
However, what happens if the array has more (or less) elements than the selection?
. if the array is longer than the selection there’s a shortfall of DOM elements and we need to add elements
. if the array is shorter than the selection there’s a surplus of DOM elements and we need to remove elements
Fortunately D3 can help in adding and removing DOM elements using two functions .enter
and .exit
.
.enter
identifies any DOM elements that need to be added when the joined array is longer than the selection.
It’s defined on an update selection (the selection returned by .data
):
d3.select('#content')
.selectAll('div')
.data(myData)
.enter();
.enter
returns an enter selection which basically represents the elements that need to be added.
It’s usually followed by .append
which adds elements to the DOM:
d3.select('#content')
.selectAll('div')
.data(myData)
.enter()
.append('div');
Let’s look at an example.
Suppose we have the following div
elements:
<divid="content">
<div></div>
<div></div>
<div></div>
</div>
and this data:
var myData=['A','B','C','D','E'];
we use .enter
and .append
to add div
elements for D and E:
d3.select('#content')
.selectAll('div')
.data(myData)
.enter()
.append('div');
View source
Note that we can join an array to an empty selection which is a very common pattern in the examples on the D3 website.
View source
.exit
returns an exit selection which consists of the elements that need to be removed from the DOM.
It’s usually followed by .remove
:
d3.select('#content')
.selectAll('div')
.data(myData)
.exit()
.remove();
Let’s repeat the example above, but using .exit
.
Starting with elements:
<divid="content">
<div></div>
<div></div>
<div></div>
</div>
and data (notice that it’s shorter than the selection):
var myData=['A'];
we use .exit
and .remove
to remove the surplus elements:
d3.select('#content')
.selectAll('div')
.data(myData)
.exit()
.remove();
View source
.style
, .attr
and .classed
.
D3 allows us to be specific about which elements are modified when new elements are entering.
We can modify:
. the existing elements
. the entering elements
. both existing and entering elements
(Most of the time the last option is sufficient, but sometimes we might want to style entering elements differently.)
The existing elements are represented by the update selection.
This is the selection returned by .data
and is assigned to u
in this example:
var myData=['A','B','C','D','E'];
var u=d3.select('#content')
.selectAll('div')
.data(myData);
u.enter()
.append('div');
u.text(function(d){
returnd;
});
View source
When the button is clicked, new elements are added, but because .text
is only called on the update selection, it’s only the existing elements that are modified.
(Note that if the button is clicked a second time, all the elements are modified.
This is because the selection will contain all 5 div
elements.
Don’t worry about this too much if you’re new here!)
The entering elements are represented by the enter selection.
This is the selection returned by .enter
.
We can modify the enter selection using:
var myData=['A','B','C','D','E'];
var u=d3.select('#content')
.selectAll('div')
.data(myData);
u.enter()
.append('div')
.text(function(d){
returnd;
});
View source
When the button is clicked, new elements are added and their text content is updated.
Only the entering elements have their text updated because we call .text
on the enter selection.
If we want to modify the existing and entering elements we could call .text
on the update and enter selections.
However D3 has a function .merge
which can merge selections together.
This means we can do the following:
var myData=['A','B','C','D','E'];
var u=d3.select('#content')
.selectAll('div')
.data(myData);
u.enter()
.append('div')
.merge(u)
.text(function(d){
returnd;
});
View source
This is a big departure from v3.
The entering elements were implicitly included in the update selection so there was no need for .merge
.
functionupdate(data){
var u=d3.select('#content')
.selectAll('div')
.data(data);
u.enter()
.append('div')
.merge(u)
.text(function(d){
returnd;
});
u.exit().remove();
}
View source
Typically the update function is called whenever the data changes.
Here’s another example where we colour entering elements orange:
functionupdate(data){
var u=d3.select('#content')
.selectAll('div')
.data(data);
u.enter()
.append('div')
.classed('new',true)
.text(function(d){
returnd;
});
u.text(function(d){
returnd;
})
.classed('new',false);
u.exit().remove();
}
View source
.data
with a key function.
This function should return a unique id value for each array element, allowing D3 to make sure each array element stays joined to the same DOM element.
Let’s look at an example, first using a key function, and then without.
We start with an array ['Z']
and each time the button is clicked a new letter is added at the start of the array.
Because of the key function each letter will stay bound to the same DOM element meaning that when a new letter is inserted each existing letter transitions into a new position:
View source
Without a key function the DOM elements’ text is updated (rather than position) meaning we lose a meaningful transition effect:
View source
There’s many instances when key functions are not required but if there’s any chance that your data elements can change position (e.g.
through insertion or sorting) and you’re using transitions then you should probably use them.
[0,2,3,5,7.5,9,10]
we can create a scale function using:
var myScale=d3.scaleLinear()
.domain([0,10])
.range([0,600]);
D3 creates a function myScale
which accepts input between 0 and 10 (the domain) and maps it to output between 0 and 600 (the range).
We can use myScale
to calculate positions based on the data:
myScale(0); // returns 0
myScale(2); // returns 120
myScale(3); // returns 180
...
myScale(10); // returns 600
View source
Scales are mainly used for transforming data values to visual variables such as position, length and colour.
For example they can transform:
. data values into lengths between 0 and 500 for a bar chart
. data values into positions between 0 and 200 for line charts
. % change data (+4%, +10%, -5% etc.) into a continuous range of colours (with red for negative and green for positive)
. dates into positions along an x-axis.
var myScale=d3.scaleLinear();
Version 4 uses a different naming convention to v3. We use d3.scaleLinear() in v4 and d3.scale.linear() in v3.As it stands the above function isn’t very useful so we can configure the input bounds (the
domain
) as well as the output bounds (the range
):
myScale
.domain([0,100])
.range([0,800]);
Now myScale
is a function that accepts input between 0 and 100 and linearly maps it to between 0 and 800.
myScale(0); // returns 0
myScale(50); // returns 400
myScale(100); // returns 800
Try experimenting with scale functions by copying code fragments and pasting them into the console or using a web-based editor such as JS Bin.
y = m * x + b
) to interpolate across the domain and range.
var linearScale=d3.scaleLinear()
.domain([0,10])
.range([0,600]);
linearScale(0); // returns 0
linearScale(5); // returns 300
linearScale(10); // returns 600
Typical uses are to transform data values into positions and lengths, so when creating bar charts, line charts (as well as many other chart types) they are the scale to use.
The output range can also be specified as colours:
var linearScale=d3.scaleLinear()
.domain([0,10])
.range(['yellow','red']);
linearScale(0); // returns "rgb(255, 255, 0)"
linearScale(5); // returns "rgb(255, 128, 0)"
linearScale(10); // returns "rgb(255, 0, 0)"
This can be useful for visualisations such as choropleth maps, but also consider scaleQuantize
, scaleQuantile
and scaleThreshold
.
y = m * x^k + b
) function.
The exponent k
is set using .exponent()
:
var powerScale=d3.scalePow()
.exponent(0.5)
.domain([0,100])
.range([0,30]);
powerScale(0); // returns 0
powerScale(50); // returns 21.21...
powerScale(100);// returns 30
scaleSqrt
scale is a special case of the power scale (where k = 0.5) and is useful for sizing circles by area (rather than radius).
(When using circle size to represent data, it’s considered better practice to set the area, rather than the radius proportionally to the data.)
var sqrtScale=d3.scaleSqrt()
.domain([0,100])
.range([0,30]);
sqrtScale(0); // returns 0
sqrtScale(50); // returns 21.21...
sqrtScale(100);// returns 30
View source
y = m * log(x) + b
) and can be useful when the data has an exponential nature to it.
var logScale=d3.scaleLog()
.domain([10,100000])
.range([0,600]);
logScale(10); // returns 0
logScale(100); // returns 150
logScale(1000); // returns 300
logScale(100000);// returns 600
View source
scaleTime
is similar to scaleLinear
except the domain is expressed as an array of dates.
(It’s very useful when dealing with time series data.)
timeScale=d3.scaleTime()
.domain([newDate(2016,0,1),newDate(2017,0,1)])
.range([0,700]);
timeScale(newDate(2016,0,1)); // returns 0
timeScale(newDate(2016,6,1)); // returns 348.00...
timeScale(newDate(2017,0,1)); // returns 700
View source
scaleSequential
is used for mapping continuous values to an output range determined by a preset (or custom) interpolator.
(An interpolator is a function that accepts input between 0 and 1 and outputs an interpolated value between two numbers, colours, strings etc.)
D3 provides a number of preset interpolators including many colour ones.
For example we can use d3.interpolateRainbow
to create the well known rainbow colour scale:
var sequentialScale=d3.scaleSequential()
.domain([0,100])
.interpolator(d3.interpolateRainbow);
sequentialScale(0); // returns 'rgb(110, 64, 170)'
sequentialScale(50); // returns 'rgb(175, 240, 91)'
sequentialScale(100);// returns 'rgb(110, 64, 170)'
View source
Note that the interpolator determines the output range so you don’t need to specify the range yourself.
The example below shows some of the other colour interpolators provided by D3:
View source
There’s also a plug-in d3-scale-chromatic which provides the well known ColorBrewer colour schemes.
scaleLinear
, scalePow
, scaleSqrt
, scaleLog
, scaleTime
and scaleSequential
allow input outside the domain.
For example:
var linearScale=d3.scaleLinear()
.domain([0,10])
.range([0,100]);
linearScale(20); // returns 200
linearScale(-10);// returns -100
In this instance the scale function uses extrapolation for values outside the domain.
If we’d like the scale function to be restricted to input values inside the domain we can ‘clamp’ the scale function using .clamp()
:
linearScale.clamp(true);
linearScale(20); // returns 100
linearScale(-10);// returns 0
We can switch off clamping using .clamp(false)
.
d3.extent
) the start and end values might not be round figures.
This isn’t necessarily a problem, but if using the scale to define an axis, it can look a bit untidy:
var data=[0.243,0.584,0.987,0.153,0.433];
var extent=d3.extent(data);
var linearScale=d3.scaleLinear()
.domain(extent)
.range([0,100]);
View source
Therefore D3 provides a function .nice()
on the scales in this section which will round the domain to ‘nice’ round values:
linearScale.nice();
View source
Note that .nice()
must be called each time the domain is updated.
scaleLinear
, scalePow
, scaleSqrt
, scaleLog
and scaleTime
usually consists of two values, but if we provide 3 or more values the scale function is subdivided into multiple segments:
var linearScale=d3.scaleLinear()
.domain([-10,0,10])
.range(['red','#ddd','blue']);
linearScale(-10); // returns "rgb(255, 0, 0)"
linearScale(0); // returns "rgb(221, 221, 221)"
linearScale(5); // returns "rgb(111, 111, 238)"
View source
Typically multiple segments are used for distinguishing between negative and positive values (such as in the example above).
We can use as many segments as we like as long as the domain and range are of the same length.
.invert()
method allows us to determine a scale function’s input value given an output value (provided the scale function has a numeric domain):
var linearScale=d3.scaleLinear()
.domain([0,10])
.range([0,100]);
linearScale.invert(50); // returns 5
linearScale.invert(100); // returns 10
A common use case is when we want to convert a user’s click along an axis into a domain value:
View source
scaleQuantize
accepts continuous input and outputs a number of discrete quantities defined by the range.
var quantizeScale=d3.scaleQuantize()
.domain([0,100])
.range(['lightblue','orange','lightgreen','pink']);
quantizeScale(10); // returns 'lightblue'
quantizeScale(30); // returns 'orange'
quantizeScale(90); // returns 'pink'
Each range value is mapped to an equal sized chunk in the domain so in the example above:
. 0 ≤ u < 25 is mapped to ‘lightblue’
. 25 ≤ u < 50 is mapped to ‘orange’
. 50 ≤ u < 75 is mapped to ‘lightgreen’
. 75 ≤ u < 100 is mapped to ‘pink’
where u is the input value.
View source
Note also that input values outside the domain are clamped so in our example quantizeScale(-10)
returns ‘lightblue’ and quantizeScale(110)
returns ‘pink’.
scaleQuantile
maps continuous numeric input to discrete values.
The domain is defined by an array of numbers:
var myData=[0,5,7,10,20,30,35,40,60,62,65,70,80,90,100];
var quantileScale=d3.scaleQuantile()
.domain(myData)
.range(['lightblue','orange','lightgreen']);
quantileScale(0); // returns 'lightblue'
quantileScale(20); // returns 'lightblue'
quantileScale(30); // returns 'orange'
quantileScale(65); // returns 'lightgreen'
View source
The (sorted) domain array is divided into n equal sized groups where n is the number of range values.
Therefore in the above example the domain array is split into 3 groups where:
. the first 5 values are mapped to ‘lightblue’
. the next 5 values to ‘orange’ and
. the last 5 values to ‘lightgreen’.
The split points of the domain can be accessed using .quantiles()
:
quantileScale.quantiles(); // returns [26.66..., 63]
If the range contains 4 values quantileScale
computes the quartiles of the data.
In other words, the lowest 25% of the data is mapped to range[0]
, the next 25% of the data is mapped to range[1]
etc.
scaleThreshold
maps continuous numeric input to discrete values defined by the range.
n-1 domain split points are specified where n is the number of range values.
In the following example we split the domain at 0
, 50
and 100
. u < 0 is mapped to ‘#ccc’
. 0 ≤ u < 50 to ‘lightblue’
. 50 ≤ u < 100 to ‘orange’
. u ≥ 100 to ‘#ccc’
where u is the input value.
var thresholdScale=d3.scaleThreshold()
.domain([0,50,100])
.range(['#ccc','lightblue','orange','#ccc']);
thresholdScale(-10); // returns '#ccc'
thresholdScale(20); // returns 'lightblue'
thresholdScale(70); // returns 'orange'
thresholdScale(110); // returns '#ccc'
View source
scaleOrdinal
maps discrete values (specified by an array) to discrete values (also specified by an array).
The domain array specifies the possible input values and the range array the output values.
The range array will repeat if it’s shorter than the domain array.
var myData=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
var ordinalScale=d3.scaleOrdinal()
.domain(myData)
.range(['black','#ccc','#ccc']);
ordinalScale('Jan'); // returns 'black';
ordinalScale('Feb'); // returns '#ccc';
ordinalScale('Mar'); // returns '#ccc';
ordinalScale('Apr'); // returns 'black';
View source
By default if a value that’s not in the domain is used as input, the scale will implicitly add the value to the domain:
ordinalScale('Monday'); // returns 'black';
If this isn’t the desired behvaiour we can specify an output value for unknown values using .unknown()
:
ordinalScale.unknown('Not a month');
ordinalScale('Tuesday');// returns 'Not a month'
D3 can also provide preset colour schemes (from ColorBrewer):
var ordinalScale=d3.scaleOrdinal()
.domain(myData)
.range(d3.schemePaired);
View source
(Note that the Brewer colour schemes are defined within a separate file d3-scale-chromatic.js.)
scaleBand
helps to determine the geometry of the bars, taking into account padding between each bar.
The domain is specified as an array of values (one value for each band) and the range as the minimum and maximum extents of the bands (e.g.
the total width of the bar chart).
In effect scaleBand
will split the range into n bands (where n is the number of values in the domain array) and compute the positions and widths of the bands taking into account any specified padding.
var bandScale=d3.scaleBand()
.domain(['Mon','Tue','Wed','Thu','Fri'])
.range([0,200]);
bandScale('Mon');// returns 0
bandScale('Tue');// returns 40
bandScale('Fri');// returns 160
The width of each band can be accessed using .bandwidth()
:
bandScale.bandwidth(); // returns 40
Two types of padding may be configured:
. paddingInner
which specifies (as a percentage of the band width) the amount of padding between each band
. paddingOuter
which specifies (as a percentage of the band width) the amount of padding before the first band and after the last band
Let’s add some inner padding to the example above:
bandScale.paddingInner(0.05);
bandScale.bandWidth(); // returns 38.38...
bandScale('Mon'); // returns 0
bandScale('Tue'); // returns 40.40...
Putting this all together we can create this bar chart:
View source
scalePoint
creates scale functions that map from a discrete set of values to equally spaced points along the specified range:
var pointScale=d3.scalePoint()
.domain(['Mon','Tue','Wed','Thu','Fri'])
.range([0,500]);
pointScale('Mon'); // returns 0
pointScale('Tue'); // returns 125
pointScale('Fri'); // returns 500
View source
The distance between the points can be accessed using .step()
:
pointScale.step(); // returns 125
Outside padding can be specified as the ratio of the padding to point spacing.
For example, for the outside padding to be a quarter of the point spacing use a value of 0.25:
pointScale.padding(0.25);
pointScale('Mon'); // returns 27.77...
pointScale.step(); // returns 111.11...
path
elements.
Each of them has a d
attribute (path data) which defines the shape of the path.
The path data consists of a list of commands (e.g.
M0,80L100,100L200,30L300,50L400,40L500,80
) such as ‘move to’ and ‘draw a line to’ (see the SVG specification for more detail).
We could create path data ourselves but D3 can help us using functions known as generators.
These come in various forms:
line | Generates path data for a multi-segment line (typically for line charts) |
area | Generates path data for an area (typically for stacked line charts and streamgraphs) |
stack | Generates stack data from multi-series data |
arc | Generates path data for an arc (typically for pie charts) |
pie | Generates pie angle data from array of data |
symbol | Generates path data for symbols such as plus, star, diamond |
d3.line()
:
var lineGenerator=d3.line();
lineGenerator
is just a function that accepts an array of co-ordinates and outputs a path data string.
So let’s go ahead and define an array of co-ordinates:
var points=[
[0,80],
[100,100],
[200,30],
[300,50],
[400,40],
[500,80]
];
and now call lineGenerator
, passing in our array:
var pathData=lineGenerator(points);
// pathData is "M0,80L100,100L200,30L300,50L400,40L500,80"
All lineGenerator
has done is create a string of M
(move to) and L
(line to) commands from our array of points.
We can now use pathData
to set the d
attribute of a path
element:
d3.select('path')
.attr('d',pathData);
View source
We can also configure our line generator in a number of ways:
. .x()
and .y()
accessor functions,
. .defined()
(to handle missing data),
. .curve
(to specify how the points are interpolated) and
. .context()
to render to a canvas element.
[0, 100]
).
However we can specify how the line generator interprets each array element using accessor functions .x()
and .y()
.
For example suppose our data is an array of objects:
var data=[
{value:10},
{value:50},
{value:30},
{value:40},
{value:20},
{value:70},
{value:50}
];
We can define the accessors like so:
lineGenerator
.x(function(d,i){
returnxScale(i);
})
.y(function(d){
returnyScale(d.value);
});
In this example we’re using the index of the array to define the x position.
Note also that we’re using scale functions:
View source
var points=[
[0,80],
[100,100],
null,
[300,50],
[400,40],
[500,80]
];
we can tell our line generator that each co-ordinate is valid only if it’s non-null:
lineGenerator
.defined(function(d){
returnd!==null;
});
Now when we call lineGenerator
it leaves a gap in the line:
View source
(Without configuring .defined
this last call returns an error.)
var lineGenerator=d3.line()
.curve(d3.curveCardinal);
View source
Although there’s a multitude of different curve types available they can be divided into two camps: those which pass through the points (curveLinear
, curveCardinal
, curveCatmullRom
, curveMonotone
, curveNatural
and curveStep
) and those that don’t (curveBasis
and curveBundle
).
See the curve explorer for more information.
.context()
function:
var context=d3.select('canvas').node().getContext('2d');
lineGenerator.context(context);
context.strokeStyle='#999';
context.beginPath();
lineGenerator(points);
context.stroke();
View source
x
and y
:
var radialLineGenerator=d3.radialLine();
var points=[
[0,80],
[Math.PI*0.25,80],
[Math.PI*0.5,30],
[Math.PI*0.75,80],
[Math.PI,80],
[Math.PI*1.25,80],
[Math.PI*1.5,80],
[Math.PI*1.75,80],
[Math.PI*2,80]
];
var pathData=radialLineGenerator(points);
View source
Accessor functions .angle()
and .radius()
are also available:
radialLineGenerator
.angle(function(d){
returnd.a;
})
.radius(function(d){
returnd.r;
});
var points=[
{a:0,r:80},
{a:Math.PI*0.25,r:80},
{a:Math.PI*0.5,r:30},
{a:Math.PI*0.75,r:80},
...
];
var pathData=radialLineGenerator(points);
y=0
and a multi-segment line defined by an array of points:
var areaGenerator=d3.area();
var points=[
[0,80],
[100,100],
[200,30],
[300,50],
[400,40],
[500,80]
];
var pathData=areaGenerator(points);
View source
We can configure the baseline using the .y0()
accessor function:
areaGenerator.y0(150);
View source
We can also feed a function into the .y0()
accessor, likewise the .y1()
accessor:
areaGenerator
.x(function(d){
returnd.x;
})
.y0(function(d){
returnyScale(d.low);
})
.y1(function(d){
returnyScale(d.high);
});
var points=[
{x:0,low:30,high:80},
{x:100,low:80,high:100},
{x:200,low:20,high:30},
{x:300,low:20,high:50},
{x:400,low:10,high:40},
{x:500,low:50,high:80}
];
Typically .y0()
defines the baseline and .y1()
the top line.
Note that we’ve also used the .x()
accessor to define the x co-ordinate.
View source
As with the line generator we can specify the way in which the points are interpolated (.curve()
), handle missing data (.defined()
) and render to canvas (.context()
);
x
and y
:
var radialAreaGenerator=d3.radialArea()
.angle(function(d){
returnd.angle;
})
.innerRadius(function(d){
returnd.r0;
})
.outerRadius(function(d){
returnd.r1;
});
var points=[
{angle:0,r0:30,r1:80},
{angle:Math.PI*0.25,r0:30,r1:70},
{angle:Math.PI*0.5,r0:30,r1:80},
{angle:Math.PI*0.75,r0:30,r1:70},
{angle:Math.PI,r0:30,r1:80},
{angle:Math.PI*1.25,r0:30,r1:70},
{angle:Math.PI*1.5,r0:30,r1:80},
{angle:Math.PI*1.75,r0:30,r1:70},
{angle:Math.PI*2,r0:30,r1:80}
];
View source
var data=[
{day:'Mon',apricots:120,blueberries:180,cherries:100},
{day:'Tue',apricots:60, blueberries:185,cherries:105},
{day:'Wed',apricots:100,blueberries:215,cherries:110},
{day:'Thu',apricots:80, blueberries:230,cherries:105},
{day:'Fri',apricots:120,blueberries:240,cherries:105}
];
var stack=d3.stack()
.keys(['apricots','blueberries','cherries']);
var stackedSeries=stack(data);
// stackedSeries = [
// [ [0, 120], [0, 60], [0, 100], [0, 80], [0, 120] ], // Apricots
// [ [120, 300], [60, 245], [100, 315], [80, 310], [120, 360] ], // Blueberries
// [ [300, 400], [245, 350], [315, 425], [310, 415], [360, 465] ] // Cherries
// ]
The .keys()
configuration function specifies which series are included in the stack generation.
The data output by the stack generator can be used however you like, but typically it’ll be used to produce stacked bar charts:
View source
or when used in conjunction with the area generator, stacked line charts:
View source
.order()
:
stack.order(d3.stackOrderInsideOut);
Each series is summed and then sorted according to the chosen order.
The possible orders are:
stackOrderNone | (Default) Series in same order as specified in .keys() |
stackOrderAscending | Smallest series at the bottom |
stackOrderDescending | Largest series at the bottom |
stackOrderInsideOut | Largest series in the middle |
stackOrderReverse | Reverse of stackOrderNone |
stack.offset(d3.stackOffsetExpand);
View source
The available offsets are:
stackOffsetNone | (Default) No offset |
stackOffsetExpand | Sum of series is normalised (to a value of 1) |
stackOffsetSilhouette | Center of stacks is at y=0 |
stackOffsetWiggle | Wiggle of layers is minimised (typically used for streamgraphs) |
stackOffsetWiggle
:
View source
var arcGenerator=d3.arc();
It can then be passed an object containing startAngle
, endAngle
, innerRadius
and outerRadius
properties to produce the path data:
var pathData=arcGenerator({
startAngle:0,
endAngle:0.25*Math.PI,
innerRadius:50,
outerRadius:100
});
// pathData is "M6.123233995736766e-15,-100A100,100,0,0,1,70.71067811865476,-70.710678
// 11865474L35.35533905932738,-35.35533905932737A50,50,0,0,0,3.061616997868383e-15,-50Z"
(startAngle
and endAngle
are measured clockwise from the 12 o’clock in radians.)
View source
innerRadius
, outerRadius
, startAngle
, endAngle
so that we don’t have to pass them in each time:
arcGenerator
.innerRadius(20)
.outerRadius(100);
pathData=arcGenerator({
startAngle:0,
endAngle:0.25*Math.PI
});
// pathData is "M6.123233995736766e-15,-100A100,100,0,0,1,70.71067811865476,-70.71067811
// 865474L14.142135623730951,-14.14213562373095A20,20,0,0,0,1.2246467991473533e-15,-20Z"
View source
We can also configure corner radius (cornerRadius
) and the padding between arc segments (padAngle
and padRadius
):
arcGenerator
.padAngle(.02)
.padRadius(100)
.cornerRadius(4);
View source
Arc padding takes two parameters padAngle
and padRadius
which when multiplied together define the distance between adjacent segments.
Thus in the example above, the padding distance is 0.02 * 100 = 2
.
Note that the padding is calculated to maintain (where possible) parallel segment boundaries.
You might ask why there isn't a single parameter padDistance for defining the padding distance. It's split into two parameters so that the pie generator (see later) doesn't need to concern itself with radius.
startAngle
, endAngle
, innerRadius
and outerRadius
e.g.
arcGenerator
.startAngle(function(d){
returnd.startAngleOfMyArc;
})
.endAngle(function(d){
returnd.endAngleOfMyArc;
});
arcGenerator({
startAngleOfMyArc:0,
endAngleOfMyArc:0.25*Math.PI
});
View source
.centroid()
for doing this:
arcGenerator.centroid({
startAngle:0,
endAngle:0.25*Math.PI
});
// returns [22.96100594190539, -55.43277195067721]
Here’s an example where .centroid()
is used to compute the label positions:
View source
var pieGenerator=d3.pie();
var data=[10,40,30,20,60,80];
var arcData=pieGenerator(data);
// arcData is an array of objects: [
// {
// data: 10,
// endAngle: 6.28...,
// index: 5,
// padAngle: 0,
// startAngle: 6.02...,
// value: 10
// },
// ...
// ]
We can then use an arc generator to create the path strings:
var arcGenerator=d3.arc()
.innerRadius(20)
.outerRadius(100);
d3.select('g')
.selectAll('path')
.data(arcData)
.enter()
.append('path')
.attr('d',arcGenerator);
Notice that the output of pieGenerator
contains the properties startAngle
and endAngle
.
These are the same properties required by arcGenerator
.
View source
The pie generator has a number of configuration functions including .padAngle()
, .startAngle()
, .endAngle()
and .sort()
.
.padAngle()
specifies an angular padding (in radians) between neighbouring segments.
.startAngle()
and .endAngle()
configure the start and end angle of the pie chart.
This allows, for example, the creation of semi-circular pie charts:
var pieGenerator=d3.pie()
.startAngle(-0.5*Math.PI)
.endAngle(0.5*Math.PI);
View source
By default the segment start and end angles are specified such that the segments are in descending order.
However we can change the sort order using .sort
:
var pieGenerator=d3.pie()
.value(function(d){returnd.quantity;})
.sort(function(a,b){
returna.name.localeCompare(b.name);
});
var fruits=[
{name:'Apples',quantity:20},
{name:'Bananas',quantity:40},
{name:'Cherries',quantity:50},
{name:'Damsons',quantity:10},
{name:'Elderberries',quantity:30},
];
View source
var symbolGenerator=d3.symbol()
.type(d3.symbolStar)
.size(80);
var pathData=symbolGenerator();
We can then use pathData to define the d
attribute of a path element:
d3.select('path')
.attr('d',pathData);
Here’s a simple chart using the symbol generator:
View source
D3 provides a number of symbol types:
View source
x
and y
values to each node such that the nodes form a tree-like shape:
{
"name": "A1",
"children": [
{
"name": "B1",
"children": [
{
"name": "C1",
"value": 100
},
{
"name": "C2",
"value": 300
},
{
"name": "C3",
"value": 200
}
]
},
{
"name": "B2",
"value": 200
}
]
}
In this section we’ll look at the tree
, cluster
, treemap
, pack
and partition
layouts.
Note that treemap
, pack
and partition
are designed to lay out hierarchies where the nodes have an associated numeric value (e.g.
revenue, population etc.).
D3 version 4 requires the hierarchical data to be in the form of a d3.hierarchy
object which we’ll cover next.
d3.hierarchy
object is a data structure that represents a hierarchy.
It has a number of functions defined on it for retrieving things like ancestor, descendant and leaf nodes and for computing the path between nodes.
It can be created from a nested JavaScript object such as:
var data={
"name":"A1",
"children":[
{
"name":"B1",
"children":[
{
"name":"C1",
"value":100
},
{
"name":"C2",
"value":300
},
{
"name":"C3",
"value":200
}
]
},
{
"name":"B2",
"value":200
}
]
}
var root=d3.hierarchy(data)
Typically you don’t need to operate on the hierarchy object itself but there are some useful functions defined on it such as:
root.descendants();
root.links()
root.descendants()
returns a flat array of root
’s descendants and root.links()
returns a flat array of objects containing all the parent-child links.
More examples of hierarchy functions
We’ll now look at the tree
, cluster
, treemap
, pack
and partition
layouts.
tree
layout arranges the nodes of a hierarchy in a tree like arrangement.
We start by creating the tree layout using:
var treeLayout=d3.tree();
We can configure the tree’s size using .size
:
treeLayout.size([400,200]);
We can then call treeLayout
, passing in our hierarchy object root
:
treeLayout(root);
This’ll write x
and y
values on each node of root
.
We can now:
. use root.descendants()
to get an array of all the nodes
. join this array to circles (or any other type of SVG element)
. use x
and y
to position the circles
and
. use root.links()
to get an array of all the links
. join the array to line (or path) elements
. use x
and y
of the link’s source and target to position the line
(In the case of root.links()
each array element is an object containing two properties source
and target
which represent the link’s source and target nodes.)
// Nodes
d3.select('svg g.nodes')
.selectAll('circle.node')
.data(root.descendants())
.enter()
.append('circle')
.classed('node',true)
.attr('cx',function(d){returnd.x;})
.attr('cy',function(d){returnd.y;})
.attr('r',4);
// Links
d3.select('svg g.links')
.selectAll('line.link')
.data(root.links())
.enter()
.append('line')
.classed('link',true)
.attr('x1',function(d){returnd.source.x;})
.attr('y1',function(d){returnd.source.y;})
.attr('x2',function(d){returnd.target.x;})
.attr('y2',function(d){returnd.target.y;});
View source
cluster
layout is very similar to the tree
layout the main difference being all leaf nodes are placed at the same depth.
var clusterLayout=d3.cluster()
.size([400,200])
var root=d3.hierarchy(data)
clusterLayout(root)
View source
treemap
layout is created using:
var treemapLayout=d3.treemap();
As usual we can configure our layout e.g.
treemapLayout
.size([400,200])
.paddingOuter(10);
Before applying this layout to our hierarchy we must run .sum()
on the hierarchy.
This traverses the tree and sets .value
on each node to the sum of its children:
root.sum(function(d){
returnd.value;
});
Note that we pass an accessor function into .sum()
to specify which property to sum.
We can now call treemapLayout
, passing in our hierarchy object:
treemapLayout(root);
The layout adds 4 properties x0
, x1
, y0
and y1
to each node which specify the dimensions of each rectangle in the treemap.
Now we can join our nodes to rect
elements and update the x
, y
, width
and height
properties of each rect
:
d3.select('svg g')
.selectAll('rect')
.data(root.descendants())
.enter()
.append('rect')
.attr('x',function(d){returnd.x0;})
.attr('y',function(d){returnd.y0;})
.attr('width',function(d){returnd.x1-d.x0;})
.attr('height',function(d){returnd.y1-d.y0;})
View source
If we’d like labels in each rectangle we could join g
elements to the array and add rect
and text
elements to each g
:
var nodes=d3.select('svg g')
.selectAll('g')
.data(rootNode.descendants())
.enter()
.append('g')
.attr('transform',function(d){return'translate('+[d.x0,d.y0]+')'})
nodes
.append('rect')
.attr('width',function(d){returnd.x1-d.x0;})
.attr('height',function(d){returnd.y1-d.y0;})
nodes
.append('text')
.attr('dx',4)
.attr('dy',14)
.text(function(d){
returnd.data.name;
})
View source
treemap
layouts can be configured in a number of ways:
. the padding around a node’s children can be set using .paddingOuter
. the padding between sibling nodes can be set using .paddingInner
. outer and inner padding can be set at the same time using .padding
. the outer padding can also be fine tuned using .paddingTop
, .paddingBottom
, .paddingLeft
and .paddingRight
.
View source
In the example above paddingTop
is 20 and paddingInner
is 2.
Treemaps can use different tiling strategies and D3 has several built in (treemapBinary
, treemapDice
, treemapSlice
, treemapSliceDice
, treemapSquarify
) and the configuration function .tile
is used to select one:
treemapLayout.tile(d3.treemapDice)
treemapBinary
strives for a balance between horizontal and vertical partitions, treemapDice
partitions horizontally, treemapSlice
partitions vertically, treemapSliceDice
alternates between horizontal and vertical partioning and treemapSquarify
allows the aspect ratio of the rectangles to be influenced.
The effect of different squarify ratios can be seen here.
pack
layout is created using:
var packLayout=d3.pack();
As usual we can configure its size:
packLayout.size([300,300]);
As with the treemap
we must call .sum()
on the hierarchy object root
before applying the pack
layout:
rootNode.sum(function(d){
returnd.value;
});
packLayout(rootNode);
The pack
layout adds x
, y
and r
(for radius) properties to each node.
Now we can add circle
elements for each descendant of root
:
d3.select('svg g')
.selectAll('circle')
.data(rootNode.descendants())
.enter()
.append('circle')
.attr('cx',function(d){returnd.x;})
.attr('cy',function(d){returnd.y;})
.attr('r',function(d){returnd.r;})
View source
Labels can be added by creating g
elements for each descendant:
var nodes=d3.select('svg g')
.selectAll('g')
.data(rootNode.descendants())
.enter()
.append('g')
.attr('transform',function(d){return'translate('+[d.x,d.y]+')'})
nodes
.append('circle')
.attr('r',function(d){returnd.r;})
nodes
.append('text')
.attr('dy',4)
.text(function(d){
returnd.children===undefined?d.data.name:'';
})
View source
The padding around each circle can be configured using .padding()
:
packLayout.padding(10)
View source
partition
layout subdivides a rectangular space into a layer for each layer of the hierarchy.
Each layer is subdivided for each node in the layer:
partition
layout is created using:
var partitionLayout=d3.partition();
As usual we can configure its size:
partitionLayout.size([400,200]);
As with the treemap
we must call .sum()
on the hierarchy object root
and before applying the partition
layout:
rootNode.sum(function(d){
returnd.value;
});
partitionLayout(rootNode);
The partition
layout adds x0
, x1
, y0
and y1
properties to each node.
We can now add rect
elements for each descendant of root
:
d3.select('svg g')
.selectAll('rect')
.data(rootNode.descendants())
.enter()
.append('rect')
.attr('x',function(d){returnd.x0;})
.attr('y',function(d){returnd.y0;})
.attr('width',function(d){returnd.x1-d.x0;})
.attr('height',function(d){returnd.y1-d.y0;});
View source
Padding can be added between nodes using .padding()
:
partitionLayout.padding(2)
View source
If we’d like to change the orientation of the partition layout so that the layers run left to right we can swap x0
with y0
and x1
with y1
when defining the rect
elements:
.attr('x',function(d){returnd.y0;})
.attr('y',function(d){returnd.x0;})
.attr('width',function(d){returnd.y1-d.y0;})
.attr('height',function(d){returnd.x1-d.x0;});
View source
We can also map the x
dimension into a rotation angle and y
into a radius to create a sunburst partition:
View source
var data=[
[10,20,30],
[40,60,80],
[100,200,300]
];
The first row represents flows from the 1st item to the 1st, 2nd and 3rd items etc.
We create the layout using:
var chordGenerator=d3.chord();
and we configure it using .padAngle()
(to set the angle between adjacent groups in radians), .sortGroups()
(to specify the order of the groups), .sortSubgroups()
(to sort within each group) and .sortChords()
to determine the z order of the chords.
We apply the layout using:
var chords=chordGenerator(data);
which returns an array of chords.
Each element of the array is an object with source
and target
properties.
Each source
and target
has startAngle
and endAngle
properties which will define the shape of each chord.
We use the ribbon
shape generator which converts the chord properties into path data (see the Shapes chapter for more information on shape generators).
var ribbonGenerator=d3.ribbon().radius(200);
d3.select('g')
.selectAll('path')
.data(chords)
.enter()
.append('path')
.attr('d',ribbonGenerator)
View source
A
, B
or C
) and add the following forces:
. all circles attract one another (to clump circles together)
. collision detection (to stop circles overlapping)
. circles are attracted to one of three centers, depending on their category
The force layout requires a larger amount of computation (typically requiring a few seconds of time) than other D3 layouts and and the solution is calculated in a step by step (iterative) manner.
Usually the positions of the SVG/HTML elements are updated as the simulation iterates, which is why we see the circles jostling into position.
forceSimulation
, passing in the array of objects
add one or more force functions (e.g.
. forceManyBody
, forceCenter
, forceCollide
) to the system
. set up a callback function to update the element positions after each tick
Let’s start with a minimal example:
var width=300,height=300
var nodes=[{},{},{},{},{}]
var simulation=d3.forceSimulation(nodes)
.force('charge',d3.forceManyBody())
.force('center',d3.forceCenter(width/2,height/2))
.on('tick',ticked);
Here we’ve created a simple array of 5 objects and have added two force functions forceManyBody
and forceCenter
to the system.
(The first of these makes the elements repel each other while the second attracts the elements towards a centre point.)
Each time the simulation iterates the function ticked
will be called.
This function joins the nodes
array to circle
elements and updates their positions:
functionticked(){
var u=d3.select('svg')
.selectAll('circle')
.data(nodes)
u.enter()
.append('circle')
.attr('r',5)
.merge(u)
.attr('cx',function(d){
returnd.x
})
.attr('cy',function(d){
returnd.y
})
u.exit().remove()
}
View source
The power and flexibility of the force simulation is centred around force functions which adjust the position and velocity of elements to achieve a number of effects such as attraction, repulstion and collision detection.
We can define our own force functions but D3 comes with a number of useful ones built in:
. forceCenter
(for setting the center of gravity of the system)
. forceManyBody
(for making elements attract or repel one another)
. forceCollide
(for preventing elements overlapping)
. forceX
and forceY
(for attracting elements to a given point)
. forceLink
(for creating a fixed distance between connected elements)
Force functions are added to the simulation using .force()
where the first argument is a user defined id and the second argument the force function:
simulation.force('charge',d3.forceManyBody())
Let’s look at the inbuilt force functions one by one.
forceCenter
is useful (if not essential) for centering your elements as a whole about a center point.
(Without it elements might disappear off the page.)
It can either be initialised with a center position:
d3.forceCenter(100,100)
or using the configuration functions .x()
and .y()
:
d3.forceCenter().x(100).y(100)
We add it to the system using:
simulation.force('center',d3.forceCenter(100,100))
See below for usage examples.
forceManyBody
causes all elements to attract or repel one another.
The strength of the attraction or repulsion can be set using .strength()
where a positive value will cause elements to attract one another while a negative value causes elements to repel each other.
The default value is -30
.
simulation.force('charge',d3.forceManyBody().strength(-20))
View source
As a rule of thumb, when creating network diagrams we want the elements to repel one another while for visualisations where we’re clumping elements together, attractive forces are necessary.
forceCollide
is used to stop elements overlapping and is particularly useful when ‘clumping’ circles together.
We must specify the radius of the elements using .radius()
:
var numNodes=100
var nodes=d3.range(numNodes).map(function(d){
return{radius:Math.random()*25}
})
var simulation=d3.forceSimulation(nodes)
.force('charge',d3.forceManyBody().strength(5))
.force('center',d3.forceCenter(width/2,height/2))
.force('collision',d3.forceCollide().radius(function(d){
returnd.radius
}))
View source
forceX
and forceY
cause elements to be attracted towards specified position(s).
We can use a single center for all elements or apply the force on a per-element basis.
The strength of attraction can be configured using .strength()
.
As an example suppose we have a number of elements, each of which has a category 0
, 1
or 2
.
We can add a forceX
force function to attract the elements to an x-coordinate 100
, 300
or 500
based on the element’s category:
simulation.force('x',d3.forceX().x(function(d){
returnxCenter[d.category];
}))
View source
Note the above example also uses forceCollide
.
If our data has a numeric dimension we can use forceX
or forceY
to position elements along an axis:
simulation.force('x',d3.forceX().x(function(d){
returnxScale(d.value);
}))
.force('y',d3.forceY().y(function(d){
return0;
}))
View source
Do use the above with caution as the x position of the elements is only approximate.
forceLink
pushes linked elements to be a fixed distance apart.
It requires an array of links that specify which elements we want to link together.
Each link object specifies a source and target element, where the value is the element’s array index:
var links=[
{source:0,target:1},
{source:0,target:2},
{source:0,target:3},
{source:1,target:6},
{source:3,target:4},
{source:3,target:7},
{source:4,target:5},
{source:4,target:7}
]
We can then pass our links array into the forceLink
function using .links()
:
simulation.force('link',d3.forceLink().links(links))
View source
The distance and strength of the linked elements can be configured using .distance()
(default value is 30) and .strength()
.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Africa"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[-6, 36], [33, 30], ... , [-6, 36]]]
}
},
{
"type": "Feature",
"properties": {
"name": "Australia"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[143, -11], [153, -28], ... , [143, -11]]]
}
},
{
"type": "Feature",
"properties": {
"name": "Timbuktu"
},
"geometry": {
"type": "Point",
"coordinates": [-3.0026, 16.7666]
}
}
]
}
In the above example we have a FeatureCollection containing an array of 3 features:
. Africa
. Australia
. the city of Timbuktu
Each feature consists of geometry (simple polygons in the case of the countries and a point for Timbuktu) and properties.
Properties can contain any information about the feature such as name, id, and other data such as population, GDP etc.
As we’ll see later, D3 takes care of most of the detail when rendering GeoJSON so you only need a basic understanding of GeoJSON to get started with D3 mapping.
[lon, lat]
) and transforms it into an x and y co-ordinate:
functionprojection(lonLat){
var x=...// some formula here to calculate x
var y=...// some formula here to calculate y
return[x,y];
}
projection([-3.0026,16.7666])
// returns [474.7594743879618, 220.7367625635119]
Projection mathematics can get quite complex but fortunately D3 provides a large number of projection functions.
For example we can create an equi-rectangular projection function using:
var projection=d3.geoEquirectangular();
projection([-3.0026,16.7666])
// returns [474.7594743879618, 220.7367625635119]
We’ll look at projections in much more detail later.
.geoPath()
and configure it with a projection function:
var projection=d3.geoEquirectangular();
var geoGenerator=d3.geoPath()
.projection(projection);
var geoJson={
"type":"Feature",
"properties":{
"name":"Africa"
},
"geometry":{
"type":"Polygon",
"coordinates":[[[-6,36],[33,30],...,[-6,36]]]
}
}
geoGenerator(geoJson);
// returns "M464.0166237760863,154.09974265651798L491.1506253268278,154.8895088551978 ...
L448.03311471280136,183.1346693994119Z"
As usual with shape generators the generated path string is used to set the d
attribute on an SVG path
element.
var geoJson={
"type":"FeatureCollection",
"features":[
{
"type":"Feature",
"properties":{
"name":"Africa"
},
"geometry":{
"type":"Polygon",
"coordinates":[[[-6,36],[33,30],...,[-6,36]]]
}
},
...
]
}
var projection=d3.geoEquirectangular();
var geoGenerator=d3.geoPath()
.projection(projection);
// Join the FeatureCollection's features array to path elements
var u=d3.select('#content g.map')
.selectAll('path')
.data(geojson.features);
// Create path elements and update the d attribute using the geo generator
u.enter()
.append('path')
.attr('d',geoGenerator);
View source
(Note that to keep things simple the GeoJSON in the above example uses just a few co-ordinates to define the country boundaries.)
The above example shows the essence of creating maps using D3 and I recommend spending time to understand each concept (GeoJSON, projections and geo generators) and how they fit together.
Now that we’ve covered the basics we’ll look at each concept in more detail.
d3.json('ne_110m_land.json',function(err,json){
createMap(json);
})
It’s worth mentioning TopoJSON which is another JSON based standard for describing geographic data and tends to result in significantly smaller file sizes.
It requires a bit more work to use, and we don’t cover it in this chapter.
However for further information check out the documentation.
geoAzimuthalEqualArea
. geoAzimuthalEquidistant
. geoGnomonic
. geoOrthographic
. geoStereographic
. geoAlbers
. geoConicConformal
. geoConicEqualArea
. geoConicEquidistant
. geoEquirectangular
. geoMercator
. geoTransverseMercator
Some projections preserve area (e.g.
geoAzimuthalEqualArea
& geoConicEqualArea
), others distance (e.g.
geoAzimuthalEquidistant
& geoConicEquidistant
) and others relative angles (e.g.
geoEquirectangular
& geoMercator
).
For a more in depth discussion of the pros and cons of each projection try resources such as Carlos A.
Furuti’s Map Projection Pages.
The grid below shows each core projection on a world map together with a longitude/latitude grid and equal radius circles.
View source
[longitude, latitude]
and outputs a pixel co-ordinate [x, y]
.
You’re free to write your own projection functions but much easier is to ask D3 to make one for you.
To do this choose a projection method (e.g.
d3.geoAzimuthalEqualArea
), call it and it’ll return a projection function:
var projection=d3.geoAzimuthalEqualArea();
projection([-3.0026,16.7666]);
// returns [473.67353385539417, 213.6120079887163]
The core projections have configuration functions for setting the following parameters:
scale | Scale factor of the projection |
center | Projection center [longitude, latitude] |
translate | Pixel [x,y] location of the projection center |
rotate | Rotation of the projection [lambda, phi, gamma] (or [yaw, pitch, roll]) |
[lon, lat]
array)
. translate specifies where the center of projection is located on the screen (with a [x, y]
array)
. rotate specifies the rotation of the projection (with a [λ, φ, γ]
array) where the parameters correspond to yaw, pitch and roll, respectively:
var projection=d3.geoAzimuthalEqualArea()
.scale(300)
.center([-3.0026,16.7666])
.translate([480,250]);
To get a feel for how each parameter behaves use the projection explorer below.
The (equal radius) circles and grid allow you to assess the projection’s distortion of area and angle.
View source
[x, y]
back to [longitude, latitude]
using the projection’s .invert()
method:
var projection=d3.geoAzimuthalEqualArea();
projection([-3.0026,16.7666])
// returns [473.67353385539417, 213.6120079887163]
projection.invert([473.67353385539417,213.6120079887163])
// returns [-3.0026, 16.766]
.fitExtent()
method sets the projection’s scale and translate such that the geometry fits within a given bounding box:
projection.fitExtent([[0,0],[900,500]],geojson);
In the example below the canvas element has a light grey background and the bounding box into which we’re fitting the geoJSON is shown as a dotted outline:
View source
If our bounding box’s top left corner is at [0, 0]
we can use the shorthand:
projection.fitSize([900,500],geojson);
geoGenerator(geoJson);
// e.g.
returns a SVG path string "M464.01,154.09L491.15,154.88 ...
L448.03,183.13Z"
We create the generator using d3.geoPath()
and usually configure it’s projection type:
var projection=d3.geoEquirectangular();
var geoGenerator=d3.geoPath()
.projection(projection);
We can now use the generator to help us create an SVG or canvas map.
The SVG option is a bit easier to implement, especially when it comes to user interaction as event handlers and hover states can be added.
The canvas approach requires a bit more work, but is typically quicker (and more memory efficient) to render.
path
elements and update the d
attribute using the geographic path generator:
var geoJson={
"type":"FeatureCollection",
"features":[
{
"type":"Feature",
"properties":{
"name":"Africa"
},
"geometry":{
"type":"Polygon",
"coordinates":[[[-6,36],[33,30],...,[-6,36]]]
}
},
...
]
}
var projection=d3.geoEquirectangular();
var geoGenerator=d3.geoPath()
.projection(projection);
// Join the FeatureCollection's features array to path elements
var u=d3.select('#content g.map')
.selectAll('path')
.data(geojson.features);
// Create path elements and update the d attribute using the geo generator
u.enter()
.append('path')
.attr('d',geoGenerator);
View source
var context=d3.select('#content canvas')
.node()
.getContext('2d');
var geoGenerator=d3.geoPath()
.projection(projection)
.context(context);
We can then begin a canvas path and call geoGenerator
which will produce the necessary canvas calls for us:
context.beginPath();
geoGenerator({type:'FeatureCollection',features:geojson.features})
context.stroke();
View source
.pointRadius()
:
var geoGenerator=d3.geoPath()
.pointRadius(5)
.projection(projection);
var feature=geojson.features[0];
// Compute the feature's area (in pixels)
geoGenerator.area(feature);
// returns 30324.86518469876
// Compute the feature's centroid (in pixel co-ordinates)
geoGenerator.centroid(feature);
// returns [266.9510120424504, 127.35819206325564]
// Compute the feature's centroid (in pixel co-ordinates)
geoGenerator.bounds(feature);
// returns [[140.6588054321928, 24.336293856408275], [378.02358370342165, 272.17304763960306]]
// Compute the path length (in pixels)
geoGenerator.measure(feature);
// returns 775.7895349902461
View source
geoGenerator({
type:'Feature',
geometry:{
type:'LineString',
coordinates:[[0.1278,51.5074],[-74.0059,40.7128]]
}
});
Circle features can be generated using d3.geoCircle()
.
Typically the center ([lon, lat]
) and the angle (degrees) between the points are set:
var circle=d3.geoCircle()
.center([0.1278,51.5074])
.radius(5);
circle();
// returns a GeoJSON object representing a circle
geoGenerator(circle());
// returns a path string representing the projected circle
A GeoJSON grid of longitude and latitude lines (known as a graticule) can be generated using d3.graticule()
:
var graticule=d3.geoGraticule();
graticule();
// returns a GeoJSON object representing the graticule
geoGenerator(graticule());
// returns a path string representing the projected graticule
(See the official documentation for detailed information on graticule configuration.)
Here’s an example using all three shapes:
View source
.geoArea()
, .geoBounds()
, .geoCentroid()
, .geoDistance()
and geoLength()
are similar to the path geometry methods described above but operate in spherical space.
d3.geoInterpolate()
method creates a function that accepts input between 0 and 1 and interpolates between two [lon, lat]
locations:
var londonLonLat=[0.1278,51.5074];
var newYorkLonLat=[-74.0059,40.7128];
var geoInterpolator=d3.geoInterpolate(londonLonLat,newYorkLonLat);
geoInterpolator(0);
// returns [0.1278, 51.5074]
geoInterpolator(0.5);
// returns [-41.182023242967695, 52.41428456719971] (halfway between the two locations)
View source
d3.geoContains
which accepts a GeoJSON feature and a [lon, lat]
array and returns a boolean:
d3.geoContains(ukFeature,[0.1278,51.5074]);
// returns true
View source